﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

namespace Game_Engine
{
    public class CharacterController : Component, IUpdateable
    {
        public enum CharacterState
        {
            Idle = 0,
            Walking = 1,
            Running = 2,
            Jumping = 3,
            Mounted = 4,
            Sliding = 5,
        }

        public CharacterState _characterState{ get; set; }
        public Boolean IsMoving{get; private set;}
        public Transform MovementAxis { get; set; }
        public Transform CameraAxis { get; set; }
        public Vector3 MoveDirection { get; set; }
        public float MoveSpeed { get; private set; }
        public float VerticalSpeed { get; set; }
        public GameObject MountObject{get; private set;}

        //Potentially modifiable attributes directly affecting gameplay
        public float RunSpeed { get; set; }
        public float JumpHeight { get; set; }
        public float Gravity { get; set; }

        //Expression of body's available actions
        public InputAxis Horizontal{get;set;}
        public InputAxis Vertical{get;set;}
        private Keys sprint = Keys.LeftShift; 
        private Keys jump = Keys.Space;

        private float walkSpeed = 10f;
        private float speedLimit = 25.0f;
        private float rotateSpeed = 1f;
        private float dragCoefficient = 0.95f;
        /*
        float fixatedRotateSpeed = 10000.0f;
        float inAirRotateSpeed = 250.0f;
        float trotAfterSeconds = 3.0f;
        */
        private bool canJump = true;
        private bool doubleJumped = false;
        private bool jumpCanceled = false;
        public bool grounded = true;
        /*
        
	
	    private float groundedTimeout = 0.25f;
	    private float lockCameraTimer = 0.0f;
        */
        private bool jumpingReachedApex = false;
        private float jumpRepeatTime = 0.5f;
        private float lastJumpTime = -1;
        private float lastJumpButtonTime = -10;
        private float jumpTimeout = 0.15f;	
        private float lastJumpStartHeight = 0;
        private float jumpButtonLength = 0;
        /*
        
        private float jumpCancelArc = 0f;
        
        


        
        
	    public bool inApex = false;
	    private Vector3 mountDirection;
	    private Vector3 mountPoint;
	    private Vector3 mountExitDirection;
	    private GameObject dummyObject;
        */
        public bool slideApexed = false;


        private bool movingBack = false;
        private float speedSmoothing = 25.0f;

        private float lastGroundedTime = 0f;
        private bool isControllable = true;
        private bool canMount = true;

        private float dot;

        private float v, h;

        //What will likely be a big constructor
        public CharacterController()
        {
            Name = "CharacterController";
            CameraAxis = new Transform();
            MovementAxis = new Transform();
            MoveDirection = MovementAxis.Forward;
            MoveSpeed = 0;
            VerticalSpeed = 0;
            RunSpeed = 50;
            JumpHeight = 0.01f;
            Gravity = 9.8f;
        }

        //The function uses input to calculate the direction and magnitude
        //of the MoveDirection vector, based on current state variables
        private void UpdateSmoothedMovementDirection()
        {
            //Camera camera = (Camera)Host.GetComponent(typeof(Camera));
            //Transform cameraTransform = camera.Transform;
            grounded = IsGrounded();

            //Forward vector relative to the camera in the x-z plane
            Vector3 forward = CameraAxis.Forward;
            forward.Normalize();

            //Left vector always relative to the camera, always orthogonal to forward
            Vector3 left = CameraAxis.Left;
            left.Normalize();

            //Handle and store our input information
            Horizontal.Update(); Vertical.Update();
            h = Horizontal.InputValue; v = Vertical.InputValue;

            //Moving backwards check
            if (v < -0.1f)
                movingBack = true;
            else
                movingBack = false;

            //This local variable controls camera locking when transitioning between
            //moving and standing still. Of questionable utility.
            //bool wasMoving = IsMoving;

            //Have we received input?
            IsMoving = Math.Abs(h) > 0.1f || Math.Abs(v) > 0.1;

            //Grounded Controls
            if (grounded)
            {
                _characterState = CharacterState.Idle;
                //Input-specified target direction relative to camera
                Vector3 targetDirection = v * forward + h * left;
                targetDirection.Normalize();

                float timeSmooth = rotateSpeed * Time.ElapsedGameTime;

                //Requires a cameracontroller class to be meaningful
                //lockCameraTimer += Time.ElapsedGameTime;
                //if(isMoving != wasMoving)
                //lockCameraTimer = 0f;

                if (IsMoving)
                {
                    //Character rotation
                    //For sufficiently small speeds the character's momentum transitions
                    //to the new direction immediately
                    /*if (MoveSpeed < 2f && grounded)
                    {
                        MoveDirection = Vector3.Normalize(targetDirection);
                    }
                    else
                    {*/
                        //To slerp, we need a quaternion that represents the rotation of targetDirection about the Up vector of the movement axis.
                        //We need the angle between the current direction and the target direction properly slerp. The dot product of the target and current
                        //directions yields this.then we have to make the quaternion representing the target direction, and interpolate by game time
                        MoveDirection.Normalize();
                        float angle = Vector3.Dot(CameraAxis.Forward, targetDirection);
                        angle = MathHelper.Clamp(angle, -1, 1);
                        angle = (float)Math.Acos(angle);
                        MovementAxis.LocalRotation = Quaternion.Slerp(MovementAxis.LocalRotation, 
                                Quaternion.CreateFromAxisAngle(MovementAxis.Up, angle), timeSmooth);
                        MoveDirection = MovementAxis.Forward;

                        //this actually works. Change to be made: left/right send in the same direction, fix it. Movement still not relative to camera. 
                        //Update CameraAxis on changes? Parenting should have fixed this so what up? TRY UPDATING CAMERAAXIS UPDATEWORLD()
                    //}
                    
                    float targetSpeed = 0;
                    targetSpeed = Math.Min(targetDirection.Length(), 1.0f);

                    //Smooth the speed based on current target direction
                    //Sets the size of iteration between current and target speed

                    if (InputManager.KeyDown(sprint))
                    {
                        targetSpeed = RunSpeed;
                        _characterState = CharacterState.Running;
                    }
                    else
                    {
                        targetSpeed = walkSpeed;
                        _characterState = CharacterState.Walking;
                    }


                    //Lerps between current and new speeds based on how we have parameterized
                    //speedSmoothing. Basically controls acceleration during turns.
                    MoveSpeed = MathHelper.Lerp(MoveSpeed, targetSpeed, timeSmooth);
                    //MoveSpeed = targetSpeed;
                    //MoveDirection = Vector3.Normalize(targetDirection);
                }
                else
                    MoveSpeed = MathHelper.Lerp(MoveSpeed, 0, timeSmooth);

                //If we are grounded we don't want to keep track of any more interactable
                //geometry, we are now an eligible bachelor of sorts
                MountObject = null;
            }
            //Sliding controls/logic
            else if (Sliding())
            { }
            //Mounted on a surface controls/logic
            else if (Mounted())
            { }
            else //In-air controls
            {
                Vector3 targetDirection = v * forward + h * left;

                //Here we find out if we are falling from having walked off an edge
                if ((_characterState == CharacterState.Idle
                        || _characterState == CharacterState.Walking
                            || _characterState == CharacterState.Running) && VerticalSpeed <= -5.0f)
                    _characterState = CharacterState.Jumping;

                if (IsMoving)
                {
                    /*Still need to find a way to emulate this vector rotation behavior
                    moveDirection = Vector3.RotateTowards(moveDirection, targetDirection, Mathf.Abs(h) 
                                        * rotateSpeed * Mathf.Deg2Rad * Time.deltaTime, 1000);*/
                    MoveDirection.Normalize();
                }
            }
        }

        void ApplyJumping()
        {
            //Prevent jumping too soon after a previous jump
            if(lastJumpTime + jumpRepeatTime > Time.TotalGameTime.TotalSeconds)
            {
                return;
            }

            if(grounded || (IsJumping() && !doubleJumped) 
                || Mounted() || Sliding())
                //The timeout permits pressing the jump button slightly before
                //landing a prior jump to 'queue' the action
                if (canJump && Time.TotalGameTime.TotalSeconds <
                        lastJumpButtonTime + jumpTimeout)
                {
                    if (Sliding())
                    { }
                    else if (Mounted())
                    { }
                    else
                        VerticalSpeed = CalculateJumpVerticalSpeed(JumpHeight);

                    //Will need to implement messaging or just call functions
                    //Ask Ravi about rationale behind messaging
                    //SendMessage("DidJump", SendMessageOptions.DontRequireReceiver);
                    DidJump();
                }
        }

        void ApplyGravity()
        {
            if(isControllable)  //Don't fuck around with a noncontrollable
            {
                //Apply gravity
                if(IsJumping() && !jumpingReachedApex && VerticalSpeed <= 1.0)
                {
                    jumpingReachedApex = true;
                    jumpCanceled = false;
                }

                if(grounded)
                {
                    VerticalSpeed = 0.0f;
                }
                else if(jumpCanceled)
                { VerticalSpeed -= Gravity * Time.ElapsedGameTime; }
                else if(!slideApexed && Sliding())
                    return;
                else if(Mounted())
                {
                    //The extra factor here breeds slower, megaman-esque falling
                    //when interacting with geometry, permitting decisions
                    VerticalSpeed -= Gravity * 0.01f * Time.ElapsedGameTime;
                }
                else
                    VerticalSpeed -= Gravity * Time.ElapsedGameTime;

                if (VerticalSpeed < -Gravity)
                    VerticalSpeed = -Gravity;
            }
        }

        //Resets all of the logic associated with controlling how a jump occurs:
        //*How jump canceling is handled, how double jumps are handled, how
        //input and time are tracked to prevent jump button spamming and odd results
        void DidJump()
        {
            if(IsJumping())
                doubleJumped = true;
            jumpingReachedApex = false;
            jumpCanceled = false;
            lastJumpTime = (float)Time.TotalGameTime.TotalSeconds;
            lastJumpStartHeight = Transform.Position.Y;
            lastJumpButtonTime = -10;

            _characterState = CharacterState.Jumping;
            if(!canMount)   //Indeed, the only time we want people mounting
                canMount = true;    //is during a jump--and this one is fresh
        }

        public void Update()
        {
            if(!isControllable)
            {
                //Just say no to inputs
                Horizontal.KillAxes();
                Vertical.KillAxes();
            }

            if(InputManager.KeyPressed(jump))
            {
                lastJumpButtonTime = (float)Time.TotalGameTime.TotalSeconds;
            }
            else if(InputManager.KeyReleased(jump) && !jumpCanceled 
                       && !jumpingReachedApex && IsJumping() && !doubleJumped)
		    {
			    jumpCanceled = true;
			    jumpButtonLength = ((float)Time.TotalGameTime.TotalSeconds - lastJumpButtonTime)*0.01f;
		    }

            UpdateSmoothedMovementDirection();

            ApplyGravity();

            ApplyJumping();

            //Calculate actual motion   --  Rotate our Vector direction by our rotation quaternion.
            Vector3 movement = MoveDirection * MoveSpeed  //Modulate by our calculate speed
                        + new Vector3(0, VerticalSpeed, 0);
 
            Rigidbody r = (Rigidbody)Host.GetComponent(typeof(Rigidbody));
            r.Velocity += movement;
            //r.Velocity *= dragCoefficient;

            if (r.Velocity.Length() >= speedLimit)
                r.Velocity = Vector3.Normalize(r.Velocity) * speedLimit;
            
            r.Update();

            if(grounded)
            {
                lastGroundedTime = (float)Time.TotalGameTime.TotalSeconds;
                if(IsJumping())
                    doubleJumped = false;
                
            }
        }

        //Next we would add collision checkers

        private float CalculateJumpVerticalSpeed(float targetJumpHeight)
        {
            // From the jump height and gravity we deduce the upwards speed 
            // for the character to reach at the apex.
            return (float)Math.Sqrt(2 * targetJumpHeight * Gravity);
        }

        bool IsGrounded()
        {
            //your guess good as mine
            //We can try to get close
            if (_characterState == CharacterState.Running || _characterState == CharacterState.Walking
                || _characterState == CharacterState.Idle)
                return true;
            else
                return false;
        }

        bool IsJumping()
        {
            return _characterState == CharacterState.Jumping;
        }

        bool Mounted()
        {
            return _characterState == CharacterState.Mounted;
        }

        bool Sliding()
        {
            return _characterState == CharacterState.Sliding;
        }
    }               
}

/*
Speeds: Add new vectors then checked magnitude for speed limit
        Add drag for properly scaled vector
 * Only apply these logics when grounded

Gravity: Separate collision update before application of physics
 * What does this even mean? Unity's controller doesn't use a FixedUpdate
 * All that needs to happen is collision logic before movement logic, meaning
 * in the game, collision logic for the player's collider before calling 
 * CharacterController's Update
*/